#! /usr/bin/env python

# -*- coding: UTF-8 -*-
# ==============================================================================
# Filename : ReaderEdit.py
# Purpose  :
# Author   : Yohann Li (lyh001686@sina.com)
# Date     : 2020-07-03 20:17
# ==============================================================================
import os
import sys

from PyQt5.QtCore import pyqtSignal, QEvent, QThread, QRegularExpression, Qt, QStringListModel, QFile, \
    QTextStream
from PyQt5.QtGui import QDragEnterEvent, QDropEvent, QTextCursor, QKeySequence
from PyQt5.QtWidgets import QApplication, QPlainTextEdit, QFileDialog, QShortcut, QCompleter, QMessageBox

from Widgets.Highlighter2 import Highlighter
from Widgets.ScrollBar import ScrollBar
from conf.configuration import logger, APPNAME


class TextEdit(QPlainTextEdit):
    open_pySignal = pyqtSignal(str)  # 发送文件路径
    title_pySignal = pyqtSignal(str)  # 设置title
    barv_pySignal = pyqtSignal(tuple)  # scroll bar value
    n_match_pySignal = pyqtSignal(int)  # 发送匹配文本数量到search bar
    finding_state_pySignal = pyqtSignal(bool)
    pass_key_event_signal = pyqtSignal(QEvent)  # 按下可输出字符的情况下，使用发送信号的方式插入字符

    def __init__(self, name: str = '', *args, **kwargs):
        super(TextEdit, self).__init__(*args, **kwargs)
        self.setLineWrapMode(QPlainTextEdit.NoWrap)
        self.setObjectName(name + '_edit')
        self.setTabStopWidth(4)
        self.setTabChangesFocus(False)
        self.setAcceptDrops(True)
        self.setAutoFillBackground(True)
        self.setProperty('find_state', 0)
        self.setAttribute(Qt.WA_InputMethodEnabled, False)
        self._file_path = ''
        self.file_name = ''
        self.setCursorWidth(1)  # 设置光标外形宽度
        self.set_shortcut()
        self.set_signal()
        self.setContentsMargins(8, 0, 8, 10)

        self.all_autocomplete_words = ['printenv', 'setenv', 'saveenv']
        self._completer = QCompleter(self)
        self._completer.setWidget(self)
        self._completer.setModel(QStringListModel(self.all_autocomplete_words, self._completer))
        self._completion_prefix = ''
        self._completer.setCompletionMode(QCompleter.PopupCompletion)
        self._completer.setCaseSensitivity(Qt.CaseInsensitive)
        self._completer.activated.connect(self._insert_completion)

        self.sBar = ScrollBar(self)
        self.setVerticalScrollBar(self.sBar)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.highlighter = Highlighter(self.document())

        self.finding_state_pySignal.connect(self.finding_state_changed)
        self.installEventFilter(self)

    def eventFilter(self, a0, a1) -> bool:
        if a1.type() == QEvent.KeyPress:
            logger.debug(f"Pressed key {a1.text()}")
        return super(TextEdit, self).eventFilter(a0, a1)

    def set_signal(self):
        pass

    @property
    def completion_prefix(self):
        return self._completion_prefix

    @completion_prefix.setter
    def completion_prefix(self, v: str):
        logger.debug(f"prefix revise from {repr(self._completion_prefix)} to {repr(v)}")
        self._completion_prefix = v

    @property
    def file_path(self):
        return self._file_path

    @file_path.setter
    def file_path(self, v: str):
        logger.debug(f"File path revise from {repr(self._file_path)} to {repr(v)}")
        self._file_path = v
        self.file_name = os.path.basename(v)

    def _insert_completion(self, c: str):  # TODO 放到Serial中的话需要在这里emit信号
        cur = self.textCursor()
        logger.debug(f"completion: {repr(c)}, prefix{repr(self.completion_prefix)}")
        extra = len(c) - len(self.completion_prefix)
        cur.insertText(c[1-extra:])
        self.setTextCursor(cur)

    def word_under_cursor(self):
        tc = self.textCursor()
        tc.select(QTextCursor.WordUnderCursor)
        # print('Here is the selected text under the cursor' , tc.selectedText())
        return tc.selectedText()

    def insert_at_end(self, text: str):
        self.moveCursor(QTextCursor.End, QTextCursor.MoveAnchor)
        self.insert(text)

    def insert(self, text: str):
        logger.debug(repr(text))
        self.insertPlainText(text)

    def set_shortcut(self):
        QShortcut(QKeySequence('Ctrl+Backspace'), self).activated.connect(self.del_line)

    def del_line(self):
        # 删除当前行
        cur = self.textCursor()
        cur.select(cur.BlockUnderCursor)
        cur.removeSelectedText()

    def finding_state_changed(self, v: bool):
        """slot"""
        self.setProperty('find_state', 1 if v else 0)
        self.style().polish(self)

    def find_text(self, find: dict):
        """
        1. 查找内容 2.
        :param find: {'exp': 'str or re', 'options': QTextDocument::FindFlags, 'direction': 'up or down', 'word': , 'case': }
        :return:
        """
        exp = find.get('exp')
        options = find.get('options')
        # logger.debug(f'Finding {repr(exp)} with options {repr(options)} ...')
        self.count_thread = CountThread(exp, self.toPlainText(), word=find.get('word'), case=find.get('case'))
        self.count_thread.count_signal.connect(self.set_n_matches)
        self.count_thread.start()

        r = self.find(exp, options) if options else self.find(exp)  # Returns true if a match was found and changes
        # the cursor to select the match; otherwise returns false.
        previous_postion = self.textCursor().position()
        if not r:  # 没有搜索到内容
            # 如果是向下搜索，则将光标移到开始位置，否则移到最后
            self.moveCursor(QTextCursor.Start, QTextCursor.MoveAnchor) \
                if find.get('direction') == 'down' else \
                self.moveCursor(QTextCursor.End, QTextCursor.MoveAnchor)
            r = self.find(exp, options) if options else self.find(exp)

        if r:
            logger.debug(f"Found {repr(self.textCursor().selectedText())}")
        else:
            cur = self.textCursor()
            cur.setPosition(previous_postion, QTextCursor.MoveAnchor)
            self.setTextCursor(cur)

    def dragEnterEvent(self, a0: QDragEnterEvent) -> None:
        if a0.mimeData().hasUrls():
            a0.accept()
        else:
            a0.ignore()

    def dropEvent(self, a0: QDropEvent) -> None:
        logger.debug(f'Source: {a0.source()}')  # expected None
        if a0.mimeData().hasUrls():
            file_path = a0.mimeData().urls()[0].toLocalFile()
            self.file_path = file_path
            self.open_pySignal.emit(file_path)
            a0.accept()
        else:
            a0.ignore()

    def set_n_matches(self, count: int):
        """receive count from thread and emit to search bar widget"""
        self.n_match_pySignal.emit(count)

    def search_bar_hide_event(self):
        logger.debug('Search bar hidden')
        # self.highlighter = Highlighter(self.document())
        self.highlighter.find_text = ''

    def save(self):
        if self.file_path:
            self.write_file(self.file_path)
        else:
            logger.warning(f"Local path {repr(self.file_path)} don't exist, switch to save as...")
            self.save_as()

    def save_as(self):
        fp, _ = QFileDialog.getSaveFileName(self, 'Save file to', os.path.expanduser('~/Desktop'), filter='Any(*);;')
        if not fp:
            return
        self.file_path = fp
        self.title_pySignal.emit(self.file_name)
        logger.debug(f"fp: {fp}")
        self.write_file(fp)

    def write_file(self, fp: str):
        logger.debug(f"Write to file, path = {repr(fp)}")
        file = QFile(fp)
        if not file.open(QFile.WriteOnly | QFile.Text):
            QMessageBox.warning(self, APPNAME, f"Cannot write file {fp}")
            return False
        cont = QTextStream(file)
        QApplication.setOverrideCursor(Qt.WaitCursor)
        cont << self.toPlainText()
        QApplication.restoreOverrideCursor()

    def keyPressEvent(self, e) -> None:
        # if e.text() in string.printable:
        #     self.pass_key_event_signal.emit(e.text())
        #     return
        # elif e.key() == Qt.Key_Tab
        self.pass_key_event_signal.emit(e)
        # return super(TextEdit, self).keyPressEvent(e)
        ctrlOrShift = e.modifiers() & (Qt.ControlModifier | Qt.ShiftModifier)
        hasModifier = (e.modifiers() != Qt.NoModifier) and not ctrlOrShift  # 非Ctrl/Shift的控制键

        # Completer可视情况下，按回车或Tab 选择item
        if self._completer.popup().isVisible() and e.key() in (Qt.Key_Enter, Qt.Key_Return, Qt.Key_Tab):
            # logger.debug(f"")
            e.ignore()
            return
        # 按下非Ctrl/Shift/Printable Key，不显示completer
        logger.debug(f"hasModifier: {repr(hasModifier)}, key: {repr(e.text())}, len: {repr(len(e.text())==0)}, keyID: {repr(e.key())}")
        if hasModifier or len(e.text()) == 0:
            # if (hasModifier or len(e.text())==0) and e.key() > 250:
            logger.debug(f"e.text len = 0; {e.text()}")
            self._completer.popup().hide()
            return super(TextEdit, self).keyPressEvent(e)# 这个return会阻挡快捷键，例如Ctrl+A, 去掉条件 len(e.text()) == 0 后暂时可以用
        # TODO What's this?
        # if ctrlOrShift and len(e.text()) == 0:
        #     return
        self.completion_prefix = self.word_under_cursor()
        if len(e.text()) != 0:
            self._completer.setCompletionPrefix(self.completion_prefix+e.text())
            self._completer.popup().setCurrentIndex(self._completer.completionModel().index(0, 0))

        cr = self.cursorRect()
        cr.setWidth(self._completer.popup().sizeHintForColumn(0)
                    + self._completer.popup().verticalScrollBar().sizeHint().width())
        self._completer.complete(cr)
        return super(TextEdit, self).keyPressEvent(e)

    def maybe_save(self):
        # logger.debug(f"modified: {self.document().isModified()}")
        if self.document().isModified():
            ret = QMessageBox.warning(self, "title won't be shown",
                                      f"{self.file_name} has been modified.\nDo you want to save your changes?",
                                      QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel)

            if ret == QMessageBox.Save:
                return self.save()

            if ret == QMessageBox.Cancel:
                return False

        return True

    def closeEvent(self, event):
        logger.debug(f"About to close...")
        if self.maybe_save():
            event.accept()
        else:
            event.ignore()

    def mousePressEvent(self, e) -> None:
        print(f"atEnd? {self.textCursor().atEnd()}, {self.textCursor().atBlockEnd()}")
        return super(TextEdit, self).mousePressEvent(e)

    # def hideEvent(self, a0) -> None:
    #     self.close()


class CountThread(QThread):
    """
    用来给搜索词计数
    """
    count_signal = pyqtSignal(int)

    def __init__(self, keyword, text: str, word: bool = False, case: bool = False):
        super(CountThread, self).__init__()
        keyword_ = keyword if not word else f"\\b{keyword}\\b"
        if type(keyword) == QRegularExpression:
            self.k = keyword
        else:
            self.k = QRegularExpression(keyword_)
            self.k.setPatternOptions(QRegularExpression.CaseInsensitiveOption) if not case else ''
        logger.debug(self.k)
        self.t = text

    def run(self) -> None:
        # logger.debug(f"thread running")
        gm = self.k.globalMatch(self.t)
        _count = 0
        while gm.hasNext():
            _ = gm.next()
            _count += 1

        # logger.debug(f"count = {_count}, emitting")
        self.count_signal.emit(_count)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    se = TextEdit()
    se.show()
    sys.exit(app.exec_())
